﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Pfz.Extensions;

namespace Pfz.DynamicObjects
{
	/// <summary>
	/// Class responsible for creating wrappers at run-time so classes that don't implement
	/// can be accessed an object that implements the interface and redirects all the calls,
	/// also implementing data-type conversion where needed.
	/// </summary>
	public sealed class AdapterGenerator
	{
		#region Inner Class - almost private
			/// <summary>
			/// This class is used internally, but it needs to be public as it is referenced
			/// directly by the generated code.
			/// </summary>
			[CLSCompliant(false)]
			public sealed class _Notifiable<T> :
				INotifyPropertyChanged
			{
				internal readonly T _value;
				internal object _sender;
				
				internal _Notifiable(T value)
				{
					_value = value;
				}

				internal void Notify(PropertyChangedEventArgs args)
				{
					var handler = PropertyChanged;
					if (handler != null)
						handler(_sender, args);
				}

				/// <summary>
				/// Implements the INotifyPropertyChanged interface.
				/// </summary>
				public event PropertyChangedEventHandler PropertyChanged;
			}
		#endregion
	
		#region Constructors
			/// <summary>
			/// Creates a new AdapterGenerator with an empty list of data-type conversions.
			/// </summary>
			public AdapterGenerator()
			{
				_conversions = new DelegatedTypeBuilderConversions();
				MustCheckIfUserInstanceIsNotNull = true;
			}
			
			/// <summary>
			/// Creates an adapter generator using the given data-type conversions.
			/// </summary>
			public AdapterGenerator(DelegatedTypeBuilderConversions conversions)
			{
				_conversions = conversions;
				MustCheckIfUserInstanceIsNotNull = true;
			}
		#endregion
		#region Properties
			#region AllowMissingMethods
				/// <summary>
				/// By default, if an interface method does not exist in the real class an immediate exception
				/// is thrown. If you set this to true, then the missing method will be generated to throw
				/// a NotSupportedException if invoked.
				/// </summary>
				public bool AllowMissingMethods { get; set; }
			#endregion
			#region ConstructorName
				/// <summary>
				/// Gets or sets a name that identifies a call to the constructor of the
				/// real type.
				/// </summary>
				public string ConstructorName { get; set; }
			#endregion
			#region Conversions
				private readonly DelegatedTypeBuilderConversions _conversions;
				/// <summary>
				/// Gets the conversions dictionary. Note that after getting a creator you should not
				/// change its contents anymore.
				/// </summary>
				public DelegatedTypeBuilderConversions Conversions
				{
					get
					{
						return _conversions;
					}
				}
			#endregion
			#region MustBeCollectible
				/// <summary>
				/// Identifies if the generated assemblies will be collectible or not.
				/// </summary>
				public bool MustBeCollectible { get; set; }
			#endregion
			#region MustCheckIfUserInstanceIsNotNull
				/// <summary>
				/// If true, the Creator will check if the passed userInstance is null or not.
				/// </summary>
				public bool MustCheckIfUserInstanceIsNotNull { get; set; }
			#endregion
		#endregion
		#region GetCastInputCreator
			/// <summary>
			/// Gets a creator for the given interface type, that accepts the origin
			/// cast as object (so it does the final cast).
			/// </summary>
			public Func<object, TInterface> GetCastInputCreator<TReal, TInterface>()
			{
				var creator = GetCreator<TReal, TInterface>();

				Func<object, TInterface> result =
					(value) => creator((TReal)value);

				return result;
			}

			private static readonly MethodInfo _GetCastInputCreatorMethod = ReflectionHelper<AdapterGenerator>.GetMethod((o) => o.GetCastInputCreator<object, IDisposable>()).GetGenericMethodDefinition();
			/// <summary>
			/// Gets a creator for the given interface type, that accepts the origin
			/// cast as object (so it does the final cast).
			/// </summary>
			public Func<object, TInterface> GetCastInputCreator<TInterface>(Type realType)
			{
				var method = _GetCastInputCreatorMethod.MakeGenericMethod(realType, typeof(TInterface));
				var result = method.Invoke(this, null);
				return (Func<object, TInterface>)result;
			}
		#endregion
		#region GetCreator
			private readonly Dictionary<KeyValuePair<Type, Type>, object> _instantiators = new Dictionary<KeyValuePair<Type, Type>, object>();
			/// <summary>
			/// Gets a creator (a delegate that creates new wrappers) for the given TReal to implement the given TInterface.
			/// </summary>
			public Func<TReal, TInterface> GetCreator<TReal, TInterface>()
			{
				object result;
				if (_instantiators.TryGetValue(new KeyValuePair<Type,Type>(typeof(TReal), typeof(TInterface)), out result))
					return (Func<TReal, TInterface>)result;

				DelegatedTypeBuilder<TReal> typeBuilder1 = null;
				DelegatedTypeBuilder<_Notifiable<TReal>> typeBuilder2 = null;
				Func<_Notifiable<TReal>, TReal> getInnerInstance = null;
				TypeBuilder emitBuilder;
				
				bool mustNotifyChanges = typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(TInterface));
				if (mustNotifyChanges)
				{
					typeBuilder2 = new DelegatedTypeBuilder<_Notifiable<TReal>>(_conversions, MustBeCollectible, new Type[] { typeof(TInterface) });
					getInnerInstance = (notifiable) => notifiable._value;
					emitBuilder = typeBuilder2._type;
				}
				else
				{
					typeBuilder1 = new DelegatedTypeBuilder<TReal>(_conversions, MustBeCollectible, new Type[] { typeof(TInterface) });
					emitBuilder = typeBuilder1._type;
				}
					
				
				if (mustNotifyChanges)
				{
					typeBuilder2.AddEvent<PropertyChangedEventHandler>
					(
						"PropertyChanged",
						(notifiable, handler) => notifiable.PropertyChanged += handler,
						(notifiable, handler) => notifiable.PropertyChanged -= handler
					);
				}

				var properties = typeof(TInterface).GetProperties().ToList();
				var methods = typeof(TInterface).GetMethods().ToList();
				var events = typeof(TInterface).GetEvents().ToList();
				
				foreach(var baseInterface in typeof(TInterface).GetInterfaces())
				{
					if (baseInterface == typeof(INotifyPropertyChanged))
						continue;
						
					properties.AddRange(baseInterface.GetProperties());
					methods.AddRange(baseInterface.GetMethods());
					events.AddRange(baseInterface.GetEvents());
				}
				
				foreach(var interfaceProperty in properties)
				{
					var getMethod = interfaceProperty.GetGetMethod();
					MethodBuilder getMethodBuilder = null;
					if (getMethod != null)
						getMethodBuilder = _CreateMethod<TReal, TInterface>(mustNotifyChanges, typeBuilder1, typeBuilder2, getMethod, getInnerInstance);

					var setMethod = interfaceProperty.GetSetMethod();
					MethodBuilder setMethodBuilder = null;
					if (setMethod != null)
						setMethodBuilder = _CreateMethod<TReal, TInterface>(mustNotifyChanges, typeBuilder1, typeBuilder2, setMethod, getInnerInstance);
						
					var propertyBuilder = emitBuilder.DefineProperty(interfaceProperty.Name, PropertyAttributes.None, interfaceProperty.PropertyType, null);
					if (getMethod != null)
						propertyBuilder.SetGetMethod(getMethodBuilder);
					
					if (setMethod != null)
						propertyBuilder.SetSetMethod(setMethodBuilder);
				}
				
				foreach(var interfaceEvent in events)
				{
					var addMethod = _CreateMethod<TReal, TInterface>(mustNotifyChanges, typeBuilder1, typeBuilder2, interfaceEvent.GetAddMethod(), getInnerInstance);
					var removeMethod = _CreateMethod<TReal, TInterface>(mustNotifyChanges, typeBuilder1, typeBuilder2, interfaceEvent.GetRemoveMethod(), getInnerInstance);
					
					var eventBuilder = emitBuilder.DefineEvent(interfaceEvent.Name, EventAttributes.None, interfaceEvent.EventHandlerType);
					eventBuilder.SetAddOnMethod(addMethod);
					eventBuilder.SetRemoveOnMethod(removeMethod);
				}
				
				foreach(var interfaceMethod in methods)
				{
					var name = interfaceMethod.Name;
					if (name == ConstructorName)
					{
						_CreateConstructorCall<TReal>(interfaceMethod, typeBuilder1, typeBuilder2);
						continue;
					}

					if (!interfaceMethod.IsSpecialName)
						_CreateMethod<TReal, TInterface>(mustNotifyChanges, typeBuilder1, typeBuilder2, interfaceMethod, getInnerInstance);
				}

				Func<TReal, TInterface> func;
				
				if (mustNotifyChanges)
				{
					var instantiator = typeBuilder2.CreateCreator();
					func =
						(userInstance) =>
						{
							if (userInstance == null)
								throw new ArgumentNullException("userInstance");
								
							var realInstance = new _Notifiable<TReal>(userInstance);
							var interfaceResult = (TInterface)instantiator(realInstance);
							realInstance._sender = interfaceResult;
							return interfaceResult;
						};
				}
				else
				{
					var instantiator = typeBuilder1.CreateCreator();

					if (MustCheckIfUserInstanceIsNotNull)
					{
						func =
							(userInstance) =>
							{
									if (userInstance == null)
										throw new ArgumentNullException("userInstance");

								return (TInterface)instantiator(userInstance);
							};
					}
					else
					{
						func =
							(userInstance) => (TInterface)instantiator(userInstance);
					}
				}
						
				_instantiators.Add(new KeyValuePair<Type,Type>(typeof(TReal), typeof(TInterface)), func);
				return func;
			}
		#endregion
		#region _CreateMethod
			private MethodBuilder _CreateMethod<TReal, TInterface>(bool mustNotifyChanges, DelegatedTypeBuilder<TReal> typeBuilder1, DelegatedTypeBuilder<_Notifiable<TReal>> typeBuilder2, MethodInfo interfaceMethod, Func<_Notifiable<TReal>, TReal> getInnerInstance)
			{
				string name = interfaceMethod.Name;
				var interfaceMethodParameterTypes = interfaceMethod.GetParameterTypes();
				var realMethod = typeof(TReal).GetMethod(name, interfaceMethodParameterTypes);

				if (realMethod == null)
					realMethod = typeof(TReal).TryGetInterfaceMethod(name);

				if (realMethod == null)
				{
					if (AllowMissingMethods)
					{
						if (mustNotifyChanges)
						{
							return typeBuilder2.AddDynamicMethod
							(
								name,
								(a, b) =>
								{
									throw new NotSupportedException("Method " + name + " is not supported.");
								},
								interfaceMethod.ReturnType,
								interfaceMethodParameterTypes
							);
						}

						return typeBuilder1.AddDynamicMethod
						(
							name,
							(a, b) =>
							{
								throw new NotSupportedException("Method " + name + " is not supported.");
							},
							interfaceMethod.ReturnType,
							interfaceMethodParameterTypes
						);
					}
				
					throw new ArgumentException("The real type " + typeof(TReal).FullName + " does not has the method " + name);
				}
				
				Action<_Notifiable<TReal>> afterAction = null;
				if (mustNotifyChanges && interfaceMethod.IsSpecialName && name.StartsWith("set_"))
				{
					var args = new PropertyChangedEventArgs(name.Substring(4));
					afterAction = 
						(notifiable) =>
							notifiable.Notify(args);
				}

				if (mustNotifyChanges)
					return typeBuilder2.AdaptToUserInstance(realMethod, name, getInnerInstance, afterAction, interfaceMethod.ReturnType, interfaceMethodParameterTypes);

				return typeBuilder1.AdaptToUserInstance(realMethod, name, null, null, interfaceMethod.ReturnType, interfaceMethodParameterTypes);
			}
		#endregion
		#region _CreateConstructorCall
			private void _CreateConstructorCall<TReal>(MethodInfo interfaceMethod, DelegatedTypeBuilder<TReal> typeBuilder1, DelegatedTypeBuilder<_Notifiable<TReal>> typeBuilder2)
			{
				if (interfaceMethod.ReturnType == typeof(void))
					throw new ArgumentException("Methods representing a call to a constructor can't return void.");

				var parameters = interfaceMethod.GetParameters();
				var constructors = typeof(TReal).GetConstructors();
				foreach (var constructor in constructors)
				{
					if (constructor.GetParameters().Length == parameters.Length)
					{
						var parameterTypes = parameters.GetTypes();

						if (typeBuilder1 != null)
							typeBuilder1.AdaptInstanceCreation(constructor, ConstructorName, interfaceMethod.ReturnType, parameterTypes);
						else
							typeBuilder2.AdaptInstanceCreation(constructor, ConstructorName, interfaceMethod.ReturnType, parameterTypes);

						return;
					}
				}

				throw new ArgumentException("Can't find a constructor with " + parameters.Length + " parameters in type " + typeof(TReal).FullName);
			}
		#endregion
	}
}
